/*******************************************************************************
 * This file is part of Crunch Graphics.
 *
 * Crunch Graphics is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Graphics is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Graphics.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.graphics.gpu;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import java.util.Arrays;

/** Encapsulates the state of the blending operations on the GPU.
 */
public class GpuBlendState extends GpuObject {
	/** Predefined blend modes for convenience.
	 */
	public enum BlendMode {
		NONE,                /**< No blending.*/
		ALPHA,               /**< Alpha blending (src_alpha, 1 - src_alpha).*/
		PREMULTIPLIED_ALPHA, /**< Premultiplied alpha blending (src_alpha, 1).*/
		ADDITIVE,            /**< Additive blending (one, one).*/
		MULTIPLICATIVE,      /**< Multiplicative blending (zero, src_color).*/
		INVERSE              /**< Inverse blending (1 - dst_color, 1 - src_color).*/
	}

	/** Sets the state of the GPU to the state represented by this object.
	 */
	public void bind() {
		getGpuState().bind(this);
	}

	/** Sets Whether blending is enabled.
	 */
	public void setBlendingEnabled(boolean e) {
		blendingEnabled = e;
	}

	/** Sets the blending equation.
	 */
	public void setBlendEquation(int equation) {
		blendEquationColor = equation;
		blendEquationAlpha = equation;
	}

	/** Sets the blending equations for color and alpha.
	 */
	public void setBlendEquations(int equationColor, int equationAlpha) {
		blendEquationColor = equationColor;
		blendEquationAlpha = equationAlpha;
	}

	/** Sets the blending function source and destination factors.
	 */
	public void setBlendFunction(int sourceFactor, int destFactor) {
		blendSourceFactorColor = sourceFactor;
		blendSourceFactorAlpha = sourceFactor;
		blendDestinationFactorColor = destFactor;
		blendDestinationFactorAlpha = destFactor;
	}

	/** Sets the blending function source and destination factors for color and alpha.
	 */
	public void setBlendFunctions(int sourceFactorColor, int destFactorColor, int sourceFactorAlpha, int destFactorAlpha) {
		blendSourceFactorColor = sourceFactorColor;
		blendSourceFactorAlpha = sourceFactorAlpha;
		blendDestinationFactorColor = destFactorColor;
		blendDestinationFactorAlpha = destFactorAlpha;
	}

	private static final int[][] BLEND_MODES = {
		{ 0,                0,                          0 },
		{ GL.GL_FUNC_ADD,   GL.GL_SRC_ALPHA,            GL.GL_ONE_MINUS_SRC_ALPHA },
		{ GL.GL_FUNC_ADD,   GL.GL_SRC_ALPHA,            GL.GL_ONE },
		{ GL.GL_FUNC_ADD,   GL.GL_ONE,                  GL.GL_ONE },
		{ GL.GL_FUNC_ADD,   GL.GL_ZERO,                 GL.GL_SRC_COLOR },
		{ GL.GL_FUNC_ADD,   GL.GL_ONE_MINUS_DST_COLOR,  GL.GL_ONE_MINUS_SRC_COLOR }
	};

	/** Sets the blending function source and destination factors using the predefined blend mode.
	 */
	public void setBlendFunction(BlendMode blendMode) {
		if (blendMode == BlendMode.NONE) {
			blendingEnabled = false;
		} else {
			blendingEnabled = true;
			blendEquationColor = BLEND_MODES[blendMode.ordinal()][0];
			blendEquationAlpha = BLEND_MODES[blendMode.ordinal()][0];
			blendSourceFactorColor = BLEND_MODES[blendMode.ordinal()][1];
			blendSourceFactorAlpha = BLEND_MODES[blendMode.ordinal()][1];
			blendDestinationFactorColor = BLEND_MODES[blendMode.ordinal()][2];
			blendDestinationFactorAlpha = BLEND_MODES[blendMode.ordinal()][2];
		}
	}

	/** Sets the blending color.
	 */
	public void setBlendColor(float[] color) {
		System.arraycopy(color, 0, blendColor, 0, 4);
	}

	/** Returns whether blending is enabled.
	 */
	public boolean isBlendingEnabled() {
		return blendingEnabled;
	}

	/** Returns the blending equation for color.
	 */
	public int getBlendEquationColor() {
		return blendEquationColor;
	}

	/** Returns the blending equation for alpha.
	 */
	public int getBlendEquationAlpha() {
		return blendEquationAlpha;
	}

	/** Returns the blending function source factor for color.
	 */
	public int getBlendFunctionSourceFactorColor() {
		return blendSourceFactorColor;
	}

	/** Returns the blending function destination factor for color.
	 */
	public int getBlendFunctionDestinationFactorColor() {
		return blendDestinationFactorColor;
	}

	/** Returns the blending function source factor for alpha.
	 */
	public int getBlendFunctionSourceFactorAlpha() {
		return blendSourceFactorAlpha;
	}

	/** Returns the blending function destination factor for alpha.
	 */
	public int getBlendFunctionDestinationFactorAlpha() {
		return blendDestinationFactorAlpha;
	}

	/** Returns the blending color.
	 */
	public float[] getBlendColor() {
		return Arrays.copyOf(blendColor, blendColor.length);
	}

	GpuBlendState(GpuState state) {
		super(state);
		blendingEnabled = false;
		blendEquationColor = GL.GL_FUNC_ADD;
		blendEquationAlpha = GL.GL_FUNC_ADD;
		blendSourceFactorColor = GL.GL_ONE;
		blendSourceFactorAlpha = GL.GL_ONE;
		blendDestinationFactorColor = GL.GL_ZERO;
		blendDestinationFactorAlpha = GL.GL_ZERO;
		Arrays.fill(blendColor, 0.0f);
	}

	/** Copy constructor.
	 */
	public GpuBlendState(GpuBlendState other) {
		super(other.getGpuState());
		blendingEnabled = other.blendingEnabled;
		blendEquationColor = other.blendEquationColor;
		blendEquationAlpha = other.blendEquationAlpha;
		blendSourceFactorColor = other.blendSourceFactorColor;
		blendSourceFactorAlpha = other.blendSourceFactorAlpha;
		blendDestinationFactorColor = other.blendDestinationFactorColor;
		blendDestinationFactorAlpha = other.blendDestinationFactorAlpha;
		System.arraycopy(other.blendColor, 0, blendColor, 0, 4);
	}

	private boolean blendingEnabled;            // whether blending is enabled
	private int blendEquationColor;             // the blend equation for color
	private int blendEquationAlpha;             // the blend equation for alpha
	private int blendSourceFactorColor;         // the source factor for color
	private int blendSourceFactorAlpha;         // the source factor for alpha
	private int blendDestinationFactorColor;    // the destination factor for color
	private int blendDestinationFactorAlpha;    // the destination factor for alpha
	private float[] blendColor = new float[4];  // the blend color

	// compares changes against current state and apply differences
	void apply(GpuBlendState currentState) {
		currentState.blendingEnabled = setStateEnabled(blendingEnabled, currentState.blendingEnabled, GL.GL_BLEND);

		if (blendingEnabled) {
			if (blendEquationColor != currentState.blendEquationColor ||
					blendEquationAlpha != currentState.blendEquationAlpha) {
				getGpuState().getGl().glBlendEquationSeparate(blendEquationColor, blendEquationAlpha);
				currentState.blendEquationColor = blendEquationColor;
				currentState.blendEquationAlpha = blendEquationAlpha;
			}

			if (blendSourceFactorColor != currentState.blendSourceFactorColor ||
					blendSourceFactorAlpha != currentState.blendSourceFactorAlpha ||
					blendDestinationFactorColor != currentState.blendDestinationFactorColor ||
					blendDestinationFactorAlpha != currentState.blendDestinationFactorAlpha) {
				getGpuState().getGl().glBlendFuncSeparate(
						blendSourceFactorColor, blendDestinationFactorColor,
						blendSourceFactorAlpha, blendDestinationFactorAlpha);
				currentState.blendSourceFactorColor = blendSourceFactorColor;
				currentState.blendSourceFactorAlpha = blendSourceFactorAlpha;
				currentState.blendDestinationFactorColor = blendDestinationFactorColor;
				currentState.blendDestinationFactorAlpha = blendDestinationFactorAlpha;
			}

			if (!Arrays.equals(blendColor, currentState.blendColor)) {
				getGpuState().getGl().glBlendColor(blendColor[0], blendColor[1], blendColor[2], blendColor[3]);
				currentState.blendColor = blendColor;
			}
		}
	}

	// sets values to the current OpenGL state
	void getCurrentState() {
		blendingEnabled = getGpuState().getGl().glIsEnabled(GL.GL_BLEND);
		int[] buf = new int[1];

		getGpuState().getGl().glGetIntegerv(GL.GL_BLEND_EQUATION_RGB, buf, 0);
		blendEquationColor = buf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_BLEND_EQUATION_ALPHA, buf, 0);
		blendEquationAlpha = buf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_BLEND_SRC_RGB, buf, 0);
		blendSourceFactorColor = buf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_BLEND_SRC_ALPHA, buf, 0);
		blendSourceFactorAlpha = buf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_BLEND_DST_RGB, buf, 0);
		blendDestinationFactorColor = buf[0];

		getGpuState().getGl().glGetIntegerv(GL.GL_BLEND_DST_ALPHA, buf, 0);
		blendDestinationFactorAlpha = buf[0];

		getGpuState().getGl().glGetFloatv(GL3.GL_BLEND_COLOR, blendColor, 0);
	}
}